package de.jmda.core.mproc;


import static de.jmda.core.mproc.ProcessingUtilities.asDeclaredType;
import static de.jmda.core.mproc.ProcessingUtilities.asTypeElement;
import static de.jmda.core.mproc.ProcessingUtilities.asVariableElement;
import static de.jmda.core.mproc.ProcessingUtilities.getPackageOf;
import static de.jmda.core.mproc.ProcessingUtilities.getTypeElement;
import static de.jmda.core.mproc.ProcessingUtilities.getTypeMirror;
import static de.jmda.core.mproc.ProcessingUtilities.isAssignableType;
import static de.jmda.core.mproc.ProcessingUtilities.isProcessingOver;
import static de.jmda.core.mproc.ProcessingUtilities.isSubtype;
import static de.jmda.core.mproc.ProcessingUtilities.isSupertype;
import static de.jmda.core.mproc.ProcessingUtilities.normalizeBinaryClassName;
import static de.jmda.core.mproc.ProcessingUtilities.typeOfAsElement;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.List;
import java.util.Set;

import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;

import org.junit.Test;

import de.jmda.core.MarkerAnnotationType;
import de.jmda.core.mproc.ProcessingUtilities;
import de.jmda.core.mproc.Processor;

public class JUTProcessingUtilities extends AbstractJUTProcessors
{
	/** This class will be processed by {@link TestProcessor} */
	@MarkerAnnotationType private class TestTypeProcessed
	{
		@SuppressWarnings("unused") private int field_int;
		@SuppressWarnings("unused") private Integer field_Integer;
		@SuppressWarnings("unused") private List<String> field_ListOfStrings;
	}

	@SupportedAnnotationTypes(value = { "de.jmda.core.MarkerAnnotationType" })
	@SupportedSourceVersion(value = SourceVersion.RELEASE_7)
	private class TestProcessor extends Processor
	{
		private TypeElement typeElement_TestTypeProcessed;
		
		private VariableElement variableElement_TestTypeProcessed_field_int;
		private VariableElement variableElement_TestTypeProcessed_field_Integer;
		private VariableElement variableElement_TestTypeProcessed_field_ListOfStrings;

		private TypeMirror typeMirror_TestTypeProcessed;
		
		private TypeMirror typeMirror_TestTypeProcessed_field_ListOfStrings;

		private TypeMirror typeMirror_Collection;
		private TypeMirror typeMirror_List;
		private TypeMirror typeMirror_TestProcessor;

		private TypeMirror typeMirror_ListOfStrings_OuterType;

		@Override
		protected boolean process()
		{
			if (isProcessingOver() == false)
			{
				typeElement_TestTypeProcessed = getTypeElement(TestTypeProcessed.class);

				for (Element enclosedElement :
				     typeElement_TestTypeProcessed.getEnclosedElements())
				{
					String simpleName = enclosedElement.getSimpleName().toString();

					VariableElement variableElement = asVariableElement(enclosedElement);

					if (variableElement != null)
					{
						if (simpleName.equals("field_int"))
						{
							variableElement_TestTypeProcessed_field_int = variableElement;
						}
						else if (simpleName.equals("field_Integer"))
						{
							variableElement_TestTypeProcessed_field_Integer = variableElement;
						}
						else if (simpleName.equals("field_ListOfStrings"))
						{
							variableElement_TestTypeProcessed_field_ListOfStrings = variableElement;
						}
						else
						{
							fail(
									"unexpected enclosed element named " + simpleName + ", " +
									"type " + variableElement.asType());
						}
					}
				}

				typeMirror_TestTypeProcessed = getTypeMirror(TestTypeProcessed.class);
				typeMirror_TestProcessor = getTypeMirror(getClass());
				typeMirror_Collection = getTypeMirror(Collection.class);
				typeMirror_List = getTypeMirror(List.class);

				typeMirror_TestTypeProcessed_field_ListOfStrings =
						typeOfAsElement(
								variableElement_TestTypeProcessed_field_ListOfStrings).asType();
				typeMirror_ListOfStrings_OuterType =
						ProcessingUtilities.getOuterType(
								typeMirror_TestTypeProcessed_field_ListOfStrings);
			}

			return false;
		}

		@Override
		protected Set<Class<? extends Annotation>> getSupposedSupportedAnnotationTypes()
		{
			return asSet(MarkerAnnotationType.class);
		}
	}

	private TestProcessor testProcessor = new TestProcessor();

	@Override
	protected List<? extends Processor> getProcessors()
	{
		return asList(testProcessor);
	}

	@Test
	public void asDeclaredTypePrivateClass()
	{
		DeclaredType declaredType =
				asDeclaredType(testProcessor.typeElement_TestTypeProcessed.asType());

		assertEquals(
				"unexpected declared type",
				declaredType.toString(), normalizeBinaryClassName(TestTypeProcessed.class));
	}

	@Test
	public void asDeclaredTypePrimitiveField()
	{
		DeclaredType declaredType =
				asDeclaredType(
						testProcessor.variableElement_TestTypeProcessed_field_int.asType());

		assertNull("unexpected declared type", declaredType);
	}

	@Test
	public void asDeclaredTypeObjectField()
	{
		DeclaredType declaredType =
				asDeclaredType(
						testProcessor.variableElement_TestTypeProcessed_field_Integer.asType());

		assertEquals(
				"unexpected declared type", Integer.class.getName(), declaredType.toString());
	}

	@Test
	public void asDeclaredTypeListOfObjectsField()
	{
		DeclaredType declaredType =
				asDeclaredType(
						testProcessor.variableElement_TestTypeProcessed_field_ListOfStrings.asType());

		assertEquals(
				"unexpected declared type", "java.util.List<java.lang.String>", declaredType.toString());
	}

	@Test
	public void asTypeElementPrivateClass()
	{
		TypeElement typeElement =
				asTypeElement(testProcessor.typeElement_TestTypeProcessed.asType());

		assertEquals(
				"unexpected type element",
				normalizeBinaryClassName(TestTypeProcessed.class),
				typeElement.getQualifiedName().toString());
	}

	@Test
	public void asTypeElementPrimitiveField()
	{
		TypeElement typeElement =
				asTypeElement(testProcessor.variableElement_TestTypeProcessed_field_int.asType());

		assertNull("unexpected type element", typeElement);
	}

	@Test
	public void asTypeElementObjectField()
	{
		TypeElement typeElement =
				asTypeElement(testProcessor.variableElement_TestTypeProcessed_field_Integer.asType());

		assertEquals(
				"unexpected type element",
				Integer.class.getName(),
				typeElement.getQualifiedName().toString());
	}

	@Test
	public void asTypeElementListOfObjectsField()
	{
		TypeElement typeElement =
				asTypeElement(testProcessor.variableElement_TestTypeProcessed_field_ListOfStrings.asType());

		assertEquals(
				"unexpected type element",
				"java.util.List",
				typeElement.getQualifiedName().toString());
	}

	@Test
	public void getFields()
	{
		TypeElement typeElement = asTypeElement(testProcessor.typeElement_TestTypeProcessed);

		List<VariableElement> fields = ProcessingUtilities.getFields(typeElement);

		assertEquals("unexpected size of fields", 3, fields.size());

		assertTrue(
				"missing variable element for int field",
				fields.contains(testProcessor.variableElement_TestTypeProcessed_field_int));
		assertTrue(
				"missing variable element for Integer field",
				fields.contains(testProcessor.variableElement_TestTypeProcessed_field_Integer));
		assertTrue(
				"missing variable element for list of strings field",
				fields.contains(testProcessor.variableElement_TestTypeProcessed_field_ListOfStrings));
	}

	@Test
	public void typeOfAsElementTestTypeProcessed()
	{
		assertEquals(
				"unexpected element",
				normalizeBinaryClassName(TestTypeProcessed.class),
				typeOfAsElement(testProcessor.typeElement_TestTypeProcessed).asType().toString());
	}

	@Test
	public void typeOfAsElementPrimitiveField()
	{
		assertNull(
				"unexpected element",
				typeOfAsElement(testProcessor.variableElement_TestTypeProcessed_field_int));
	}

	@Test
	public void typeOfAsElementObjectField()
	{
		assertEquals(
				"unexpected element",
				Integer.class.getName(),
				typeOfAsElement(testProcessor.variableElement_TestTypeProcessed_field_Integer).asType().toString());
	}

	@Test
	public void typeOfAsElementListOfObjectsField()
	{
		assertEquals(
				"unexpected element",
				"java.util.List<E>",
				typeOfAsElement(testProcessor.variableElement_TestTypeProcessed_field_ListOfStrings).asType().toString());
	}

	@Test
	public void getPackageOfElement()
	{
		assertEquals(
				"unexpected package name for private class",
				getClass().getPackage().getName(),
				getPackageOf(testProcessor.typeElement_TestTypeProcessed).asType().toString());
	}

	@Test
	public void typeMirrorCollection()
	{
		// type mirrors add generic type parameter
		String expected = Collection.class.getName() + "<E>";
		assertEquals(
				"unexpected type mirror for " + Collection.class.getName(),
				expected,
				testProcessor.typeMirror_Collection.toString());
	}

	@Test
	public void typeMirrorTestProcessor()
	{
		// type mirrors add generic type parameter
		String expected = normalizeBinaryClassName(TestProcessor.class);
		assertEquals(
				"unexpected type mirror for " + Collection.class.getName(),
				expected,
				testProcessor.typeMirror_TestProcessor.toString());
	}

	@Test
	public void typeMirrorTestTypeProcessed()
	{
		// type mirrors add generic type parameter
		String expected = normalizeBinaryClassName(TestTypeProcessed.class);
		assertEquals(
				"unexpected type mirror for " + Collection.class.getName(),
				expected,
				testProcessor.typeMirror_TestTypeProcessed.toString());
	}

	@Test
	public void isSubtypeStringObject()
	{
		assertTrue(
				"missing sub type relation of string and object",
				isSubtype(getTypeMirror(String.class), getTypeMirror(Object.class)));
	}

	@Test
	public void isNotSubtypeListCollection()
	{
		assertFalse(
				"unexpected sub type relation of list and collection (see subtype definition in Java spec)",
				isSubtype(
						getTypeMirror(List.class), getTypeMirror(Collection.class)));
	}

	@Test
	public void isAssignableTypeStringObject()
	{
		assertTrue(
				"missing assignable type relation of string and object",
				isAssignableType(
						getTypeMirror(String.class), getTypeMirror(Object.class)));
	}

	@Test
	public void isNotAssignableTypeListCollection()
	{
		assertFalse(
				"unexpected assignable type relation of list and collection (see subtype definition in Java spec)",
				isAssignableType(
						getTypeMirror(List.class), getTypeMirror(Collection.class)));
	}

	@Test
	public void isSupertypeObjectString()
	{
		assertTrue(
				"missing super type relation of string and object",
				isSupertype(
						getTypeMirror(Object.class), getTypeMirror(String.class)));
	}

	@Test
	public void isSupertypeCollectionList()
	{
		assertTrue(
				"missing super type relation of collection and list",
				isSupertype(
						testProcessor.typeMirror_Collection, testProcessor.typeMirror_List));
	}

	@Test
	public void getOuterType()
	{
		assertEquals(
				testProcessor.typeMirror_ListOfStrings_OuterType.toString(),
				testProcessor.typeMirror_List.toString());
	}

	@Test
	public void hasTypeParameters()
	{
		assertTrue(ProcessingUtilities.hasTypeParameters(testProcessor.typeMirror_Collection));
		assertEquals(
				// type mirror version
				ProcessingUtilities.hasTypeParameters(testProcessor.typeMirror_Collection),
				// declared type version
				ProcessingUtilities.hasTypeParameters(
						asDeclaredType(
								testProcessor.variableElement_TestTypeProcessed_field_ListOfStrings.asType())));
	}
}